package be.rivendale.mathematics;

import org.apache.commons.lang.Validate;

public class Plane {
    private Point a;
    private Point b;
    private Point c;

    public Plane(Point a, Point b, Point c) {
        validatePlane(a, b, c);
        this.a = a;
        this.b = b;
        this.c = c;
    }

    /**
     * Checks that the basic rules that define a plane are met.
     * These rules include:
     * <ul>
     * <li>{@link #validatePointNotNull(Point) None of the points are null}</li>
     * <li>{@link #validatePlanePointsNotEqual(Point , Point , Point) None of the points are equal}</li>
     * <li>{@link #validatePlanePointsNonLinear(Point , Point , Point) All points do not lie in one line}</li>
     * </ul>
     * @param a Point a
     * @param b Point b
     * @param c Point c
     * @throws IllegalArgumentException If at least one of the rules is not met.
     */
    private void validatePlane(Point a, Point b, Point c) throws IllegalArgumentException {
        validatePointNotNull(a);
        validatePointNotNull(b);
        validatePointNotNull(c);
        validatePlanePointsNotEqual(a, b, c);
        validatePlanePointsNonLinear(a, b, c);
    }

    /**
     * Validates that a point must not be null
     * @param point The point to check for null
     * @throws IllegalArgumentException When the point is null
     */
    private void validatePointNotNull(Point point) throws IllegalArgumentException {
        Validate.notNull(point, "The points of a plane must not be null");
    }

    /**
     * Checks that none of the plane's points are equal.
     * A plane is only defined if none of the three points that defines it are equal.
     * @param a Point a
     * @param b Point b
     * @param c Point c
     * @throws IllegalArgumentException When either two all are three points are equal.
     */
    private void validatePlanePointsNotEqual(Point a, Point b, Point c) throws IllegalArgumentException {
        String errorMessage = "A plane is only defined if none of the points are equal";
        Validate.isTrue(!a.equals(b), errorMessage);
        Validate.isTrue(!a.equals(c), errorMessage);
        Validate.isTrue(!b.equals(c), errorMessage);
    }

    /**
     * Checks that the three plane points do not lie in one line.
     * @param a Point a.
     * @param b Point b.
     * @param c Point c.
     * @throws IllegalArgumentException When the three points lie in one line.
     */
    private void validatePlanePointsNonLinear(Point a, Point b, Point c) throws IllegalArgumentException {
        Vector vectorAToB = Triple.vectorBetweenPoints(a, b);
        Vector vectorAtoC = Triple.vectorBetweenPoints(a, c);
        Validate.isTrue(!vectorAToB.isParallelTo(vectorAtoC), "A plane is only defined if all three points are non linear");
    }

    public Point getA() {
        return a;
    }

    public Point getB() {
        return b;
    }

    public Point getC() {
        return c;
    }

    /**
     * Returns an unnormalized normal vector of this plane.
     * <p>This normal vector can be obtained by calculating the cross product between two vectors on the plane.
     * These two vectors can in turn be calculated by each subtracting two points on the plane from eachother.
     * This gives the following formula: <code>(b - a) x (c - a)</code> where 'x' means the cross product.</p>
     * <p><strong>Note:</strong> since this vector is not normalized, this is by no means a unique vector.
     * Although this method will always return the same normal, it can also be calculated with other methods
     * (for example taking vectors between other points on the plane to calculate the cross product with).
     * This may result in an other valid normal vecor.
     * It is guaranteed however, that these alternate normal calulation approaches all yield vectors that are parallel
     * to eachother (they may point to opposite directions though).
     * @return A unnormalized normal vector of this plane.
     */
    public Vector normal() {
        return Triple.vectorBetweenPoints(a, b).crossProduct(Triple.vectorBetweenPoints(a, c));
    }

    /**
     * Checks if the specified point lies on this plane.
     * <p>This test is implemented as follows:
     * The point is only on the plane if a vector between the specified point and any of the three points
     * {@link #getA() a}, {@link #getB() b}, {@link #getC() c} that define the plane, is perpendicular to the
     * plane's normal.</p>
     * @param point The point to check if it lies on this plane.
     * @return True if the point lies on this plane.
     */
    public boolean contains(Point point) {
        return MathematicalUtilities.equals(point, getA())
                || normal().isPerpendicularTo(Triple.vectorBetweenPoints(getA(), point));
    }

    /**
     * Calculates a new point somewhere on the plane, by using the two parameters that project the position of the point
     * in the plane's 2D space.
     * <p>This is done by solving the parametric equation <code>r = r0 + s*u + t*v</code> for this plane. See also
     * <a href="http://en.wikipedia.org/wiki/Plane_(geometry)#Define_a_plane_with_a_point_and_two_vectors_lying_on_it">here</a>.
     * In this equation, r0 is any given point on the plane (in our case we take point {@link #getA() a}). u and v are
     * two vectors that lie on the plane but are not parallel. Finally, s and t are two scalar values (in which the
     * plane eqation is parametric) that multiply the vectors, so that they define some position "in the extention of
     * the two vectors".</p>
     * <p>We c+
     * an see the two vectors as the axes (something like X and Y, but they do not have to be perpendicular) with
     * origin x0 on which we project (or translate) a new point in the plane's 2D space. We then convert this 2D
     * position into a 3D position by adding the point x0, and calculate the position that is indicated in 2D by s and t
     * relative to the length of the axis vectors.</p>
     * @param aToBCoordinate Coordinate of the first axis vector on the plane's 2D space. (this value is often known as
     * 's' in the plane's parametric equation.
     * @param aToCCoordinate Coordinate of the second axis vector on the plane's 2D space. (this value is often known as
     * 't' in the plane's parametric equation.
     * @return A new point that is:
     * <ul><li>somewhere on the plane</li><li>aToBCoordinate times the length of the vector between point a and b</li>
     * <li>aToCCoordinate times the length of the vector between point a and c</li></ul>
     * One side effect of this is that when passing both parameters as 0, the resulting point will be equal to point a.
     */
    public Point pointOnPlane(double aToBCoordinate, double aToCCoordinate) {
        Vector vectorAToB = Triple.vectorBetweenPoints(a, b);
        Vector vectorAToC = Triple.vectorBetweenPoints(a, c);
        return getA().add(vectorAToB.multiply(aToBCoordinate).add(vectorAToC.multiply(aToCCoordinate)).asPoint());
    }
}
